﻿/*
   Copyright 2014 Dmitry Bratus

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

using Flower.Directory.Host;
using Flower.Services;
using Flower.Services.Data;
using Jurassic;
using Jurassic.Library;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;

namespace Flower.Directory.Scripting
{
    public sealed class ScriptRunner
    {
        private class ScriptParameterGetter : IScriptParameterVisitor
        {
            private object _value;
            public object Value { get { return _value; } }

            public void Visit(BoolScriptParameter param)
            {
                _value = param.Value;
            }

            public void Visit(IntScriptParameter param)
            {
                _value = param.Value;
            }

            public void Visit(DoubleScriptParameter param)
            {
                _value = param.Value;
            }

            public void Visit(StringScriptParameter param)
            {
                _value = param.Value;
            }

            public void Visit(DirectoryEntryScriptParameter param)
            {
                _value = param.Value;
            }

            public void Visit(BoolArrayScriptParameter param)
            {
                _value = param.Value;
            }

            public void Visit(IntArrayScriptParameter param)
            {
                _value = param.Value;
            }

            public void Visit(DoubleArrayScriptParameter param)
            {
                _value = param.Value;
            }

            public void Visit(StringArrayScriptParameter param)
            {
                _value = param.Value;
            }

            public void Visit(DirectoryEntryArrayScriptParameter param)
            {
                _value = param.Value;
            }
        }

        private static readonly Lazy<Dictionary<string, MethodInfo>> _clrScripts =
            new Lazy<Dictionary<string, MethodInfo>>(GetClrScripts);

        private readonly IScriptEnginePool _scriptEnginePool;
        private readonly IDirectory _directory;
        private readonly ConcurrentDictionary<ScriptEngine, DirectoryObject> _directoryObjectsPool =
            new ConcurrentDictionary<ScriptEngine, DirectoryObject>();
        private readonly ConcurrentDictionary<ScriptEngine, ObjectInstance> _inputObjectsPool =
            new ConcurrentDictionary<ScriptEngine, ObjectInstance>();
        private readonly ConcurrentDictionary<ScriptEngine, ObjectInstance> _outputObjectsPool =
            new ConcurrentDictionary<ScriptEngine, ObjectInstance>();

        public ScriptRunner(IScriptEnginePool scriptEnginePool, IDirectory directory)
        {
            _scriptEnginePool = scriptEnginePool;
            _directory = directory;
        }

        private static Dictionary<string, MethodInfo> GetClrScripts()
        {
            var clrScripts = new Dictionary<string, MethodInfo>();

            foreach (Type type in typeof(ScriptRunner).Assembly.GetTypes())
            {
                foreach (MethodInfo meth in type.GetMethods())
                {
                    if (meth.GetCustomAttribute(typeof(ClrScriptAttribute)) != null)
                    {
                        clrScripts.Add(type.Name, meth);
                    }
                }
            }

            return clrScripts;
        }

        public ICollection<ScriptParameter> ExecuteScript(string userName, string script, ICollection<ScriptParameter> args)
        {
            var transactionOptions =
                new TransactionOptions
                {
                    IsolationLevel = (Transaction.Current == null) ? IsolationLevel.ReadCommitted : Transaction.Current.IsolationLevel,
                    Timeout = TimeSpan.FromMinutes(1)
                };

            ICollection<ScriptParameter> result;

            using (var trn = new TransactionScope(TransactionScopeOption.Required, transactionOptions))
            {
                result = (script.StartsWith("@")) ? ExecuteClrScript(script, args) : ExecuteJsScript(userName, script, args);
                trn.Complete();
            }

            return result;
        }

        private ICollection<ScriptParameter> ExecuteClrScript(string script, ICollection<ScriptParameter> args)
        {
            MethodInfo meth;

            if (!_clrScripts.Value.TryGetValue(script.Substring(1), out meth))
            {
                throw new ArgumentException("script", string.Format("Unknown script {0}.", script));
            }

            var parameters = meth.GetParameters();
            var scriptArgs = new object[parameters.Length];
            var scriptParameterGetter =
                new ScriptParameterGetter();

            scriptArgs[0] = _directory;

            for (int i = 1; i < parameters.Length; i++)
            {
                ScriptParameter scriptParam =
                    args.FirstOrDefault(p => p.Name == parameters[i].Name);

                if (scriptParam != null)
                {
                    scriptParam.AcceptVisitor(scriptParameterGetter);
                    scriptArgs[i] = scriptParameterGetter.Value;
                }
            }

            var scriptResult = 
                meth.Invoke(null, scriptArgs) as Dictionary<string, object>;

            var result = new List<ScriptParameter>();

            if (scriptResult != null)
            {
                foreach (var scriptResultParam in scriptResult)
                {
                    var resultParam = 
                        JsConvert.MakeScriptParameter(scriptResultParam.Key, scriptResultParam.Value);

                    if (resultParam != null)
                    {
                        result.Add(resultParam);
                    }
                    else
                    {
                        if (scriptResultParam.Value != null)
                        {
                            throw new InvalidCastException
                            (
                                string.Format
                                (
                                    "Failed to convert output parameter of type {0}.", 
                                    resultParam.GetType()
                                )
                            );
                        }
                    }
                }
            }

            return result;
        }

        private ICollection<ScriptParameter> ExecuteJsScript(string userName, string script, ICollection<ScriptParameter> args)
        {
            ScriptEngine scriptEngine = _scriptEnginePool.GetScriptEngine(userName);

            try
            {
                scriptEngine.SetGlobalValue
                (
                    "$dir",
                    _directoryObjectsPool.GetOrAdd(scriptEngine, eng => new DirectoryObject(eng, _directory))
                );

                var input = _inputObjectsPool.GetOrAdd(scriptEngine, eng => eng.Object.Construct());
                var output = _outputObjectsPool.GetOrAdd(scriptEngine, eng => eng.Object.Construct());

                scriptEngine.SetGlobalValue("$in", JsConvert.ScriptParamsToObject(input, args));
                scriptEngine.SetGlobalValue("$out", output);

                scriptEngine.Execute(script);

                ICollection<ScriptParameter> result =
                    JsConvert
                        .ObjectToScriptParams
                        (
                            scriptEngine.GetGlobalValue<ObjectInstance>("$out")
                        )
                        .ToList();

                JsConvert.CleanupObject(input);
                JsConvert.CleanupObject(output);

                return result;
            }
            finally
            {
                _scriptEnginePool.ReturnScriptEngine(userName, scriptEngine);
            }
        }
    }
}
